﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.Contracts;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Json;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;

using MetroLog;
using MishraReader.Entities.Tools;


namespace MishraReader.Entities
{
    public enum GoogleReaderConnectorStatus
    {
        NotConnected,
        Connecting,
        Connected,
    }


    public enum LoadItemType
    {
        ReadingList,
        Unread,
        Starred,
    }

    [DebuggerDisplay("Status = {_status}, UserName = {UserName}, ConnectedAt = {ConnectedAt}")]
    internal class GoogleReaderConnector : Notifier, IGoogleReaderConnector
    {
        private const string Client = "MishraReader";


        private const string GoogleReaderApiUrl = "https://www.google.com/reader/api/0/";
        private static readonly ILogger Log = LogManagerFactory.DefaultLogManager.GetLogger<GoogleReaderConnector>();
        private readonly IErrorService _errorService;
        private readonly SemaphoreSlim _resourcesLimitator = new SemaphoreSlim(2);
        private readonly AsyncLock _lock = new AsyncLock();

        private string _auth;
        private string _password;

        private string _savedToken;
        private GoogleReaderConnectorStatus _status;
        private DateTime _tokenDate;

        public GoogleReaderConnector(IErrorService errorService)
        {
            _errorService = errorService;
            MaxItemsCount = 200;
            Status = GoogleReaderConnectorStatus.NotConnected;
        }

        public DateTime ConnectedAt { get; private set; }
        public int MaxItemsCount { get; set; }

        public GoogleReaderConnectorStatus Status
        {
            get { return _status; }
            private set
            {
                Set(ref _status, value);
            }
        }

        public string UserName { get; private set; }

        public Task Connect(string login, string pass)
        {
            return InternalConnect(login, pass);
        }

        public void Disconnect()
        {
            _auth = null;
            _password = null;
            UserName = null;
            _savedToken = null;
            _tokenDate = default(DateTime);
            ConnectedAt = default(DateTime);

            Status = GoogleReaderConnectorStatus.NotConnected;
        }

        public async Task<SubscriptionsList> GetSubscriptionsListAsync()
        {
            SubscriptionsList subscriptionsList = null;

            Log.Debug("GetSubscriptionsList ...");

            var result = await InternalSendRequest(
                string.Format(@"subscription/list?output=json&ck={0}&client={1}", DateTime.UtcNow.ToUnixTime(), Client)).ConfigureAwait(false);

            if (result == null)
            {
                Log.Error("Did not get a response for the subscription list");
                return new SubscriptionsList();
            }

            Log.Debug("GetSubscriptionsList ... response");

            var serializer = new DataContractJsonSerializer(typeof(SubscriptionsList));
            subscriptionsList = (SubscriptionsList)serializer.ReadObject(result);
            if (subscriptionsList == null)
            {
                Log.Error("Could not deserialize subscriptions list");
                return new SubscriptionsList();
            }

            Log.Debug("GetSubscriptionsList ... deserialized");


            foreach (var subscription in subscriptionsList.Subscriptions)
            {
                subscription.Initialize(subscriptionsList);

                // Categories
                subscriptionsList.ProcessCategories(subscription);


                Debug.Assert(subscription.Category != null);
            }

            subscriptionsList.FinalizeInit();
            // Load unread items

            var refreshRequired = await UpdateUnreadCountAsync(subscriptionsList).ConfigureAwait(false);

            Log.Debug("GetSubscriptionsList ... loaded");

            return subscriptionsList;
        }

        /// <summary>
        ///   Loads all items and returns downloaded items
        /// </summary>
        /// <param name="subscriptionsList"> </param>
        /// <param name="category"> </param>
        /// <param name="continuationToken"> </param>
        /// <param name="pageSize"> </param>
        /// <returns> </returns>
        public Task<ItemsList> LoadAllItemsAsync(SubscriptionsList subscriptionsList, IGoogleReaderStream category,
                                      string continuationToken, int pageSize)
        {
            return DoLoadItems(subscriptionsList, category, continuationToken, pageSize, false);
        }


        /// <summary>
        ///   Loads starred items and returns downloaded items
        /// </summary>
        /// <param name="subscriptionsList"> </param>
        /// <param name="continuationToken"> </param>
        /// <param name="pageSize"> </param>
        /// <returns> </returns>
        public Task<ItemsList> LoadStarredItemsAsync(SubscriptionsList subscriptionsList, string continuationToken, int pageSize)
        {
            var requestUrl =
                String.Format(
                    "stream/contents/user/-/state/com.google/starred?&r=n&likes=false&comments=false&n={0}{1}&ck={2}&client={3}",
                    pageSize,
                    String.IsNullOrEmpty(continuationToken) ? String.Empty : String.Concat("&c=", continuationToken),
                    DateTime.UtcNow.ToUnixTime(),
                    Client);

            return LoadItems(requestUrl, subscriptionsList, LoadItemType.Starred);
        }

        /// <summary>
        ///   Loads unread items and returns downloaded items
        /// </summary>
        /// <param name="subscriptionsList"> </param>
        /// <param name="category"> </param>
        /// <param name="continuationToken"> </param>
        /// <param name="pageSize"> </param>
        /// <returns> </returns>
        public Task<ItemsList> LoadUnreadItemsAsync(SubscriptionsList subscriptionsList, IGoogleReaderStream category,
                                         string continuationToken, int pageSize)
        {
            return DoLoadItems(subscriptionsList, category, continuationToken, pageSize, true);
        }

        public async Task MarkAllAsReadAsync(IGoogleReaderStream subscription)
        {
            var token = await GetToken().ConfigureAwait(false);
            var now = DateTime.UtcNow.ToUnixTime().ToString(CultureInfo.InvariantCulture);

            var parameters = new Dictionary<string, string>();
            parameters["s"] = subscription == null
                                  ? "user/-/state/com.google/reading-list"
                                  : subscription.Id;
            parameters["T"] = token.Substring(2);
            parameters["ts"] = now;

            var requestUrl = string.Format("mark-all-as-read?client={0}", Client);

            var result = await PostRequest(requestUrl, parameters);

            if (!result)
                throw new GoogleReaderException("Could not mark all as read");

        }

        public async Task MarkItemAsReadAsync(Item item)
        {
            var token = await GetToken().ConfigureAwait(false);
            await EditTag(item, token, Tuple.Create("read", true), Tuple.Create("kept-unread", false));
        }

        public async Task MarkItemAsStarredAsync(Item item)
        {
            var token = await GetToken().ConfigureAwait(false);
            await EditTag(item, token, Tuple.Create("starred", true));
        }

        public async Task MarkItemAsUnreadAsync(Item item)
        {
            var token = await GetToken().ConfigureAwait(false);
            await EditTag(item, token, Tuple.Create("kept-unread", true), Tuple.Create("read", false));
            await EditTag(item, token, Tuple.Create("tracking-kept-unread", true));
        }

        public async Task SetSortPreferenceAsync(bool desc)
        {
            var token = await GetToken().ConfigureAwait(false);

            var parameters = new Dictionary<string, string>();
            parameters["s"] = "user/-/state/com.google/reading-list";
            parameters["k"] = "ranking";
            parameters["v"] = desc ? "newest" : "oldest";
            parameters["T"] = token.Substring(2);

            var requestUrl =
                string.Format("preference/stream/set?client={0}",
                            Client);

            var result = await PostRequest(requestUrl, parameters);

            if (!result)
                throw new GoogleReaderException("Unable to change the sort preference");
                         
        }

        public async Task UnmarkItemAsStarredAsync(Item item)
        {
            var token = await GetToken().ConfigureAwait(false);
            await EditTag(item, token, Tuple.Create("starred", false));
        }

        public async Task<Tuple<bool, int>> UpdateUnreadCountAsync(SubscriptionsList subscriptionsList)
        {
            var countProxy = 0;
            var refreshRequied = false;
            countProxy = 0;

            try
            {
                subscriptionsList.Subscriptions.ForEach(s => s.BeginEdit());

                // Get the list of Unreads
                var unreadList = await LoadUnreadList();
                foreach (var unreadItem in unreadList.Unreads)
                {
                    var item = unreadItem;

                    // Find the subscription that owns the unread item
                    var subscription = subscriptionsList.Subscriptions.FirstOrDefault(s => s.Id == item.Id);
                    if (subscription != null)
                    {
                        var unreadCount = Convert.ToInt32(unreadItem.Count);

                        // If the unread count isn't what we aleady have, increment the
                        // new items counter
                        if (unreadCount != subscription.ServerUnreadsCount)
                        {
                            if (unreadCount > subscription.UnreadsCount)
                                countProxy += unreadCount - subscription.UnreadsCount;

                            refreshRequied = true;
                        }

                        // Must be refresh (ServerUnreadsCountChanged)

                        // Update the unread count on our subsription from the server unreads
                        subscription.ServerUnreadsCount = unreadCount;

                        var timeSpan = Int64.Parse(unreadItem.NewestItemTimestampUsec);
                        if (subscription.NewestItemTimestampUsec < timeSpan)
                            refreshRequied = true;

                        // Also store the timestamp of the most recent item
                        subscription.NewestItemTimestampUsec = timeSpan;
                    }
                }

                // Once we've updated each subscription with the latest
                // unread count and timestamp, and there are no unread items,
                // mark all items as read ???

                // server count changed it set at false in the begin update
                // if the server count is never set, it'll be false, so this looks
                // like cleanup

                foreach (var subscription in subscriptionsList.Subscriptions)
                {
                    if (subscription.ServerUnreadsCountChanged == false)
                        //no more unread items
                    {
                        if (subscription.ServerUnreadsCount != 0 || subscription.LocalReadCount != 0)
                        {
                            subscription.ServerUnreadsCount = 0;

                            subscription.MarkAllAsRead(false);

                            refreshRequied = true;
                        }
                    }
                }
            }
            finally
            {
                subscriptionsList.Subscriptions.ForEach(s => s.EndEdit());
            }

            // Return the number of new unread items
            return Tuple.Create(refreshRequied, countProxy);
        }

        private void AddAuthHeader(HttpClient client)
        {
            client.DefaultRequestHeaders.Add("Authorization", "GoogleLogin auth=" + _auth);
        }

        private Task<ItemsList> DoLoadItems(SubscriptionsList subscriptionsList,
                                      string id,
                                      string continuationToken,
                                      int pageSize, bool unreadOnly)
        {
            var token = string.IsNullOrEmpty(continuationToken) ? string.Empty : string.Concat("&c=", continuationToken);
            var unreadReq = unreadOnly ? "&xt=user%2F-%2Fstate%2Fcom.google%2Fread" : string.Empty;


            var requestUrl =
                string.Format("stream/contents/{4}?{5}&r=n&likes=false&comments=false&n={0}{1}&ck={2}&client={3}",
                              pageSize,
                              token,
                              DateTime.UtcNow.ToUniversalTime(),
                              Client,
                              id,
                              unreadReq);

            return LoadItems(requestUrl, subscriptionsList, unreadOnly ? LoadItemType.Unread : LoadItemType.ReadingList);
        }

        private Task<ItemsList> DoLoadItems(SubscriptionsList subscriptionsList,
                                      IGoogleReaderStream category,
                                      string continuationToken,
                                      int pageSize, bool unreadOnly)
        {
            //Contract.Assert(category == null || category.Id != SubscriptionsList.NoCategoryId);
            if (category != null && category.Id == SubscriptionsList.NoCategoryId)
                return Task.FromResult(new ItemsList(Enumerable.Empty<Item>())); // return empty here as there's no way to get a list of uncategorieze articles

            var catId = category == null ? "user/-/state/com.google/reading-list" : Uri.EscapeDataString(category.Id);

            return DoLoadItems(subscriptionsList, catId, continuationToken, pageSize, unreadOnly);
        }

        private async Task EditTag(Item item, string token, params Tuple<string, bool>[] tags)
        {

            var tagPairs = tags.Select(pair => new {Key = (pair.Item2 ? "a" : "r"), Value = "user/-/state/com.google/" + pair.Item1});

            var parameters = tagPairs.ToDictionary(kv => kv.Key, kv => kv.Value);
            parameters["async"] = "true";
            parameters["s"] = item.Subscription.Id;
            parameters["i"] = item.Id;
            parameters["T"] = token.Substring(2);


            var requestUrl = string.Format("edit-tag?client={0}&ck={1}", Client,
                                           DateTime.UtcNow.Subtract(TimeSpan.FromDays(3650)).ToUnixTime());

            var success = await PostRequest(requestUrl, parameters).ConfigureAwait(false);
            if (!success)
                throw new GoogleReaderException(string.Format("Could not change state of item {0}", item.Id));
        }

        private void Error(string message, Exception ex)
        {
            Log.Error(message, ex);
            _errorService.HandleException(ex);
        }

        private async Task<string> GetToken()
        {
            if (!string.IsNullOrEmpty(_savedToken) && DateTime.UtcNow.Subtract(_tokenDate).TotalMinutes < 1)
            {
                return _savedToken;
            }

            var requestUrl = string.Format("token?ck={0}&client={1}", DateTime.UtcNow.ToUnixTime(), Client);
            var result = await InternalSendRequest(requestUrl).ConfigureAwait(false);

            if (result == null)
            {
                return null;
            }
            using (var reader = new StreamReader(result))
            {
                _savedToken = await reader.ReadToEndAsync();
                _tokenDate = DateTime.UtcNow;
                return _savedToken;
            }
        }

        private async Task InternalConnect(string login, string pass)
        {
            Status = GoogleReaderConnectorStatus.Connecting;

            UserName = login;
            _password = pass;

            var formData = new Dictionary<string, string>();

            formData["service"] = "reader";
            formData["accountType"] = "HOSTED_OR_GOOGLE";
            formData["Email"] = UserName;
            formData["Passwd"] = _password;
            formData["continue"] = "http://www.google.com/";

            using (var wc = new HttpClient())
            {
                var content = new FormUrlEncodedContent(formData);

                try
                {
                    var httpResponse = await wc.PostAsync(@"https://www.google.com/accounts/ClientLogin", content);
                    httpResponse.EnsureSuccessStatusCode();
                    var response = await httpResponse.Content.ReadAsStringAsync().ConfigureAwait(false);

                    _auth = ParseAuthToken(response);

                    ConnectedAt = DateTime.UtcNow;
                    Status = GoogleReaderConnectorStatus.Connected;
                }
                catch (Exception)
                {
                    Status = GoogleReaderConnectorStatus.NotConnected;
                    throw;
                }
            }
        }

        private async Task<Stream> InternalSendRequest(string apiCallUrl)
        {
            try
            {
                await _resourcesLimitator.WaitAsync();
                using (var http = new HttpClient {MaxResponseContentBufferSize = 1024*1024*5})
                {
                    AddAuthHeader(http);

                    var response = await http.GetStreamAsync(GoogleReaderApiUrl + apiCallUrl).ConfigureAwait(false);
                    var ms = new MemoryStream();
                    await response.CopyToAsync(ms);
                    ms.Position = 0;

                    return ms;
                }
            }
            catch (Exception ex)
            {
                Error("InternalSendRequest", ex);

                return null;
            }
            finally
            {
                _resourcesLimitator.Release();
            }
        }

        private async Task<ItemsList> LoadItems(String requestUrl, SubscriptionsList subscriptionsList, LoadItemType loadItemType)
        {
            ItemsList list = null;

            var result = await InternalSendRequest(requestUrl).ConfigureAwait(false);
                             
            if (result == null)
                return null;

            try
            {
                var serializer = new DataContractJsonSerializer(typeof(ItemsList));
                list = (ItemsList)serializer.ReadObject(result);
            }
            catch (Exception ex)
            {
                Error("LoadReadingListItems", ex);
            }

            using (await _lock.LockAsync())
            {
                try
                {
                    subscriptionsList.Subscriptions.ForEach(s => s.BeginEdit());

                    foreach (var item in list.Items)
                    {
                        var local = item;

                        local.Initialize();

                        var subscription = subscriptionsList.Subscriptions
                            .FirstOrDefault(s => s.Id == local.Origin.StreamId);

                        if (subscription != null)
                            subscription.AddItem(item, loadItemType);
                    }
                }
                finally
                {
                    subscriptionsList.Subscriptions.ForEach(s => s.EndEdit());
                }
            }

            return list;
                          
        }

        /// <summary>
        ///   Load the unread list
        /// </summary>
        /// <returns> </returns>
        private async Task<UnreadsList> LoadUnreadList()
        {
            UnreadsList unreadsList = null;

            var unreadCountResult = await InternalSendRequest(@"unread-count?allcomments=true&output=json").ConfigureAwait(false);

            var serializer = new DataContractJsonSerializer(typeof(UnreadsList));
            unreadsList = (UnreadsList)serializer.ReadObject(unreadCountResult);

            return unreadsList;
        }

        private async Task<bool> PostRequest(string requestUrl, IEnumerable<KeyValuePair<string, string>> nameValueCollection)
        {
            using (var wc = new HttpClient())
            {
                AddAuthHeader(wc);

                try
                {
                    var formContent = new FormUrlEncodedContent(nameValueCollection);

                    var httpResponse = await wc.PostAsync(GoogleReaderApiUrl + requestUrl, formContent).ConfigureAwait(false);

                    return httpResponse.IsSuccessStatusCode;
                }
                catch (Exception ex)
                {
                    Error("PostRequest Url = " + requestUrl, ex);
                }
                return false;
            }
        }

        /// <summary>
        ///   Parse the Auth token from the response.
        /// </summary>
        /// <param name="response"> </param>
        /// <returns> </returns>
        private static string ParseAuthToken(string response)
        {
            // Get the auth token.
            string auth;
            try
            {
                auth = new Regex(@"Auth=(?<auth>\S+)").Match(response).Result("${auth}");
            }
            catch (Exception ex)
            {
                throw new AuthTokenException(ex.Message);
            }

            // Validate token.
            if (string.IsNullOrEmpty(auth))
            {
                throw new AuthTokenException("Missing or invalid 'Auth' token.");
            }

            // Use this token in the header of each new request.
            return auth;
        }

        /* public void AddSubscription(string title, string feed, SubscriptionCategory[] categories, SubscriptionsList subscriptionsList, Action<Subscription> nextAction)
         {
             Task.Factory.StartNew(() => GetToken(token =>
             {
                 string postData = "ac=subscribe";
                 postData += "&s=feed/" + HttpUtility.UrlEncode(feed);
                 if (!string.IsNullOrEmpty(title))
                     postData += "&t=" + HttpUtility.UrlEncode(title);

                 postData += "&T=" + token.Substring(2);

                 string requestUrl = string.Format("subscription/edit?client={0}", Client);

                 if (!PostRequest(requestUrl, postData))
                 {
                     //the subcription failed (network error or feed not correct)
                     nextAction(null);
                 }

                 SendRequest(string.Format(@"stream/contents/feed/{2}?output=json&ck={0}&client={1}", DateTime.UtcNow.ToUnixTime(), Client, HttpUtility.UrlEncode(feed)), result =>
                 {
                     //the subcription failed (network error or feed not correct)
                     if (result == null)
                     {
                         nextAction(null);
                         return;
                     }

                     DataContractJsonSerializer serializer = new DataContractJsonSerializer(typeof(Subscription));
                     Subscription subscription = (Subscription)serializer.ReadObject(result);

                     subscription.Categories = new List<SubscriptionCategory>();
                     subscription.Initialize(subscriptionsList, this);

                     if (categories.Length == 0)
                     {
                         subscriptionsList.AffectNoCategory(subscription);
                     }

                     subscriptionsList.AddNewSubscription(subscription, false);

                     UpdateSubscription(subscription, subscription.Title, categories, () => nextAction(subscription));
                 }, true);

             }));
         }*/

        /*  public void DeleteSubscription(Subscription subscription, Action nextAction)
          {
              string postData = "ac=unsubscribe";
              postData += "&s=" + HttpUtility.UrlEncode(subscription.ID);
              postData += "&T=" + token.Substring(2);

              string requestUrl = string.Format("subscription/edit?client={0}", Client);

              PostRequest(requestUrl, postData);

              Tools.CurrentContext.Send(state => nextAction(), null);
          }*/
        /*

      

                public void UpdateSubscription(Subscription subscription, string newTitle, SubscriptionCategory[] subscriptionCategories, Action nextAction)
                {
                    GetToken(token =>
                                 {
                                     string postData = "ac=edit";
                                     postData += "&s=" + HttpUtility.UrlEncode(subscription.ID);
                                     postData += "&st" + HttpUtility.UrlEncode(newTitle);
                                     postData += "&T=" + token.Substring(2);

                                     var toRemove = subscription.Categories.Where(c => c.ID != SubscriptionsList.NoCategoryID && !subscriptionCategories.Contains(c));
                                     var toAdd = subscriptionCategories.Where(c => c.ID != SubscriptionsList.NoCategoryID && !subscription.Categories.Contains(c));

                                     Tools.CurrentContext.Send(state =>
                                     {
                                         foreach (SubscriptionCategory category in subscription.Categories.Where(c => !subscriptionCategories.Contains(c)))
                                         {
                                             category.RemoveSubscription(subscription);
                                         }
                                     }, null);

                                     postData = toRemove.Aggregate(postData, (current, category) => current + ("&r=" + HttpUtility.UrlEncode(category.ID)));
                                     postData = toAdd.Aggregate(postData, (current, category) => current + ("&a=user/-/label/" + HttpUtility.UrlEncode(category.Label)));

                                     string requestUrl = string.Format("subscription/edit?client={0}", Client);

                                     PostRequest(requestUrl, postData);

                                     Tools.CurrentContext.Send(state =>
                                        {
                                            subscription.Title = newTitle;
                                            subscription.Categories.Clear();

                                            if (subscriptionCategories.Length == 0)
                                                subscription.SubscriptionsList.AffectNoCategory(subscription);
                                            else
                                                subscription.Categories.AddRange(subscriptionCategories);

                                            subscription.SubscriptionsList.ProcessCategories(subscription);
                                            nextAction();
                                        }, null);
                                 });
                }
        */
    }


    public class AuthTokenException : Exception
    {
        public AuthTokenException()
        {
        }

        public AuthTokenException(string message) : base(message)
        {
        }

        public AuthTokenException(string message, Exception inner) : base(message, inner)
        {
        }
    }

    public class GoogleReaderException : Exception
    {
        //
        // For guidelines regarding the creation of new exception types, see
        //    http://msdn.microsoft.com/library/default.asp?url=/library/en-us/cpgenref/html/cpconerrorraisinghandlingguidelines.asp
        // and
        //    http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dncscol/html/csharp07192001.asp
        //

        public GoogleReaderException()
        {
        }

        public GoogleReaderException(string message) : base(message)
        {
        }

        public GoogleReaderException(string message, Exception inner) : base(message, inner)
        {
        }
    }
}
